/**
* Created with CLion.
* User:Lenovo
* Date:2023-03-18
* Time:13:55
* Description:自定义类型：结构体，枚举，联合
*/

#include <stdio.h>
#include <stddef.h>

#pragma pack(1)
struct SS
{
    char c1;
    int i;
    char c2;
};
# pragma pack()

//定义学生类型
struct Stu
{
    //成员变量
    char name[20];
    int age;
    float weight;
} s4, s5, s6;//结构体类型的全局变量
struct Stu s7;//这也是全局变量

//匿名结构体类型 - 不能创建局部变量
struct
{
    char c;
    int a;
    double d;
} s0;//可以创建全局变量

struct
{
    char c;
    int a;
    double d;
} *ps;

typedef struct
{
   int data;
   char c;
} S;// S 是结构体的类型名

//链表 - 结构体的自引用
struct Node
{
    int data;
    struct Node *next;
};

//错误写法
/*
typedef struct
{
    int data;
    Node0 *next;
} Node0;
*/

struct B
{
    float f;
    S s;//嵌套
};

struct S
{
    char name[100];
    int *ptr;
};

struct S1
{
    char c;
    int a;
};

struct S2
{
    char c1;
    int a;
    char c2;
};

struct S2_
{
    char c1;
    char c2;
    int a;
};

struct S3
{
    char c1;
    int a;
    char c2;
    char c3;
};

struct S4
{
    double d;
    char c;
    int i;
};

struct S5
{
    char c1;
    struct S4 s4;
    double d;
};

//位段 - 二进制位(bit 位)
struct A
{
    int _a : 2;
    int _b : 5;
    int _c : 10;
    int _d : 30;
};

struct SA
{
    char a : 3;
    char b : 4;
    char c : 5;
    char d : 4;
};

enum Sex
{
    //枚举的可能取值，默认是从 0 开始，递增 1 的
    MALE = 5,
    FEMALE,
    SECRET
};

union Un
{
    char c;
    int i;
};

union Un0
{
    char arr[5];
    int i;
};

int main()
{
    // C 语言允许自己创造一些类型：结构体、枚举、联合
    //结构的声明
    /*
    struct tag
    {
        member-list;
    } variable-list;
    */

    {
        struct Stu s1;//局部变量
        struct Stu s2;
        struct Stu s3;
    }
    {
        /* ps = &s0; */ // 两种结构体不是同一种，会报警告
    }

    {
        struct Node n1;
        struct Node n2;
        n1.next = &n2;
    }

    //结构体变量的初始化
    {
        S s2 = {100, 'q'};
        S s3 = {.c = 'r', .data = 2000};//按照自己的顺序进行初始化
        struct B sb = {3.14F, {200, 'w'}};//结构体嵌套初始化

        printf("%.2f, %d, %c\n", sb.f, sb.s.data, sb.s.c);
    }

    {
        int a = 100;
        struct S s = {"abcdef", NULL};
    }

    //结构体内存对齐
    {
        printf("%zd\n", sizeof(struct S1));// 8
        printf("%zd\n", sizeof(struct S2));// 12
        printf("%zd\n", sizeof(struct S3));// 12
        // 5 6 7 ?

        //结构体对齐规则：
        // 1.结构体的第一个成员永远都放在 0 偏移处
        // 2.从第二个成员开始，以后的每个成员都要对齐到某个对齐数的整数倍处
            //这个对齐数是：成员自身大小和默认对齐数的较小值
            // ps：VS 环境下，默认对齐数是 8
            //     gcc 环境下，没有默认对齐数，此时对齐数就是成员自身的大小
        // 3.当成员全部存放进去后，结构体的总大小必须是所有成员的对齐数中最大对齐数的整数倍
            //如果不够，则浪费空间对齐
        // 4.如果嵌套了结构体，嵌套的结构体要对齐到自己成员的最大对齐数的整数倍处
            //整个结构体的大小必须是最大对齐数的整数倍，最大对齐数包含嵌套的结构体成员中的对齐数

        //如何计算偏移量 - offsetof 宏 offsetof(TYPE, MEMBER)
        printf("%zd\n", offsetof(struct S1, c));// 0
        printf("%zd\n", offsetof(struct S1, a));// 4
        //这说明：存放字符 c 的后面浪费了三个字节的空间

        printf("%zd\n", sizeof(struct S2_));// 8
        //成员变量的位置不同，结构体的大小可能不同

        printf("%zd\n", sizeof(struct S4));// 16
        printf("%zd\n", sizeof(struct S5));// 32
        // S4 的最大对齐数是 8，所以 s4 的偏移量是 8，前 7 个空间被浪费
        printf("%zd\n", offsetof(struct S5, s4));// 8

        //为什么存在内存对齐？
        // 1.平台原因(移植原因)：不是所有的硬件平台都能访问任意地址上的任意数据的；某些硬件平台只能在某些地址处取某些特定类型的数据，否则抛出硬件异常
        // 2.性能原因：数据结构(尤其是栈)应该尽可能地在自然边界上对齐，原因在于：为了访问未对齐的内存，处理器需要做两次内存访问，而访问对齐的内存仅需要做一次内存访问
        //总结：内存对齐是一种以空间换时间的做法
        //尽量节省空间：让占用空间小的成员尽量集中在一起

        // 32 位的机器，一次读取 32 个 bit 位的内存空间
        // 64 位的机器同理

        //修改默认对齐数
        // #pragma pack(num) - 设置默认对齐数为 num
        // #pragma pack() - 还原默认对齐数
        printf("%zd\n", sizeof(struct SS));// 6
        //设置默认对齐数是 1：不存在对齐
    }

    //结构体传参
    {
        /* void print1(struct S s) //传值调用
         * {
         *     printf("%d\n", s.num);
         * }
         *
         * void print2(struct S *ps) //传址调用
         * {
         *     printf("%d", ps -> num);
         * }
         * */
        //上面两种传参方式哪一个更好？
        //传址调用 - 函数传参的时候，参数需要压栈，会有时间和空间上的系统开销，如果传递的结构体过大，会导致性能下降
    }

    //结构体实现位段
    {
        //位段的声明和结构是类似的，有两个不同
        // 1.位段的成员必须是 int，unsigned int 和 signed int，或者是 char
        // 2.位段的成员名后面有一个冒号和一个数字
        struct A sa;
        printf("%zd\n", sizeof(sa));// 8
        //冒号后面的数字表示的是这个变量占有的二进制位数
        //比如 int _a : 2，说明 _a 占两个二进制位，说明 _a 只能表示 0 1 2 3

        //位段的内存分配
        //位段的空间上是按照需要以 1 个字节(char)或 4 个字节(int)的方式来开辟的，即每次开辟 1/4 个字节，用完了之后再分配新的
        //位段涉及很多不确定因素，位段是不跨平台的，注重可移植的程序应该避免使用位段
            // 1.int 位段被当成是有符号数还是无符号数是不确定的
            // 2.位段中最大位的数目不能确定：16 位的机器是 16 位，32 位的机器是 32 位
            // 3.位段中的成员在内存中从左向右分配还是从右向左分配是未定义的(先分配后面还是先分配前面)
            // 4.当一个结构包含两个位段，第二个位段成员比较大，无法容纳第一个位段剩余的位时，是舍弃剩余的位还是利用，这是不确定的

        struct SA sa1 = { 0 };
        sa1.a = 10;// 1010 - 010
        sa1.b = 12;// 1100 - 1100
        sa1.c = 3;// 011 - 00011
        sa1.d = 4;// 100 - 0100
        // 0110 0010 0000 0011 0000 0100
        // 62 03 04
        //说明：CLion 对于位段的分配规则是
        // 1.分配到的内存中的 bit 位是由右向左使用
        // 2.分配的内存剩余的 bit 位不够使用时，浪费掉
        //不代表其他的编译器环境下也是这样分配的

        //位段的应用：32 位 IP 地址的封装
    }

    //枚举 - 把可能的取值一一列举
    {
        // enum Sex s = MALE;
        printf("%d\n", MALE);  // 0( + 5)
        printf("%d\n", FEMALE);// 1( + 5)
        printf("%d\n", SECRET);// 2( + 5)

        //应用：重命名数字，写菜单(menu)增加代码可读性
        //优点：增加代码的可读性和可维护性；和 #define 相比有类型检查，更严谨；便于调试；使用方柏霓，一次可以定义多个常量
    }

    //联合(共用体) - union
    //联合的成员变量共用同一块空间 - 重叠 -> 同一时间只能使用一个成员
    //联合变量的大小至少是最大成员的大小(因为联合体至少得有能力保存最大的成员)
    {
        union Un u;
        printf("%zd\n", sizeof(u));// 4
        printf("%p\n", &u);        // 0x7ffffcb84
        printf("%p\n", &(u.i));    // 0x7ffffcb84
        printf("%p\n", &(u.c));    // 0x7ffffcb84
    }
    //利用联合体判断大小端
    {
        union Un u;
        u.i = 1;
        if (u.c == 1) // 01 00 00 00     00 00 00 01 ?
        {
            printf("小端\n");
        }
        else
        {
            printf("大端\n");
        }
    }
    //联合体大小的计算
    {
        printf("%zd\n", sizeof(union Un0));// 8
    }
    //联合体的内存访问也存在对齐，当最大成员大小不是最大对齐数的整数倍的时候，就要对齐到最大对齐数的整数倍


    return 0;
}

